use-same-route-navigation.spec.tsx 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. import type { NextRouter } from 'next/router';
  2. import { useRouter } from 'next/router';
  3. import type { IPagePopulatedToShowRevision } from '@growi/core';
  4. import { renderHook, waitFor } from '@testing-library/react';
  5. import { vi } from 'vitest';
  6. import { mock } from 'vitest-mock-extended';
  7. import { useFetchCurrentPage } from '~/states/page';
  8. import { useSetEditingMarkdown } from '~/states/ui/editor';
  9. import { useSameRouteNavigation } from './use-same-route-navigation';
  10. // Mock dependencies
  11. vi.mock('next/router', () => ({
  12. useRouter: vi.fn(),
  13. }));
  14. vi.mock('~/states/page');
  15. vi.mock('~/states/ui/editor');
  16. // Define stable mock functions outside of describe/beforeEach
  17. const mockFetchCurrentPage = vi.fn();
  18. const mockSetEditingMarkdown = vi.fn();
  19. const pageDataMock = mock<IPagePopulatedToShowRevision>({
  20. revision: {
  21. body: 'Test page content',
  22. },
  23. });
  24. describe('useSameRouteNavigation', () => {
  25. // Define a mutable router object that can be accessed and modified in tests
  26. let mockRouter: { asPath: string };
  27. beforeEach(() => {
  28. // Clear mocks and reset implementations before each test
  29. vi.clearAllMocks();
  30. // Initialize the mutable router object
  31. mockRouter = {
  32. asPath: '',
  33. };
  34. // Mock useRouter to return our mutable router object
  35. (useRouter as ReturnType<typeof vi.fn>).mockReturnValue(
  36. mockRouter as NextRouter,
  37. );
  38. (useFetchCurrentPage as ReturnType<typeof vi.fn>).mockReturnValue({
  39. fetchCurrentPage: mockFetchCurrentPage,
  40. });
  41. (useSetEditingMarkdown as ReturnType<typeof vi.fn>).mockReturnValue(
  42. mockSetEditingMarkdown,
  43. );
  44. mockFetchCurrentPage.mockResolvedValue(pageDataMock);
  45. });
  46. it('should call fetchCurrentPage and mutateEditingMarkdown on path change', async () => {
  47. // Arrange: Initial render (SSR case - no fetch on initial render)
  48. mockRouter.asPath = '/initial-path';
  49. const { rerender } = renderHook(() => useSameRouteNavigation());
  50. // Assert: No fetch on initial render (useRef previousPath is null)
  51. expect(mockFetchCurrentPage).not.toHaveBeenCalled();
  52. expect(mockSetEditingMarkdown).not.toHaveBeenCalled();
  53. // Act: Simulate CSR navigation to a new path
  54. mockRouter.asPath = '/new-path';
  55. rerender();
  56. // Assert
  57. await waitFor(() => {
  58. // 1. fetchCurrentPage is called with the new path
  59. expect(mockFetchCurrentPage).toHaveBeenCalledWith({ path: '/new-path' });
  60. // 2. mutateEditingMarkdown is called with the content from the fetched page
  61. expect(mockSetEditingMarkdown).toHaveBeenCalledWith(
  62. pageDataMock.revision?.body,
  63. );
  64. });
  65. });
  66. it('should not trigger effects if the path does not change', async () => {
  67. // Arrange: Initial render
  68. mockRouter.asPath = '/same-path';
  69. const { rerender } = renderHook(() => useSameRouteNavigation());
  70. // Initial render should not trigger fetch (previousPath is null initially)
  71. expect(mockFetchCurrentPage).not.toHaveBeenCalled();
  72. expect(mockSetEditingMarkdown).not.toHaveBeenCalled();
  73. // Act: Rerender with the same path (simulates a non-navigation re-render)
  74. rerender();
  75. // Assert: Still no fetch because path hasn't changed
  76. await new Promise((resolve) => setTimeout(resolve, 100));
  77. expect(mockFetchCurrentPage).not.toHaveBeenCalled();
  78. expect(mockSetEditingMarkdown).not.toHaveBeenCalled();
  79. });
  80. it('should not call mutateEditingMarkdown if pageData or revision is null', async () => {
  81. // Arrange: Initial render
  82. mockRouter.asPath = '/initial-path';
  83. const { rerender } = renderHook(() => useSameRouteNavigation());
  84. // Initial render should not trigger fetch
  85. expect(mockFetchCurrentPage).not.toHaveBeenCalled();
  86. // Arrange: Mock fetch to return null for the next navigation
  87. mockFetchCurrentPage.mockResolvedValue(null);
  88. // Act: Navigate to a new path
  89. mockRouter.asPath = '/path-with-no-data';
  90. rerender();
  91. // Assert
  92. await waitFor(() => {
  93. // fetch should be called
  94. expect(mockFetchCurrentPage).toHaveBeenCalledWith({
  95. path: '/path-with-no-data',
  96. });
  97. // but mutate should not be called because pageData is null
  98. expect(mockSetEditingMarkdown).not.toHaveBeenCalled();
  99. });
  100. });
  101. });